package sym.qrcode;

import sym.qrcode.BinaryBitmap;
import sym.qrcode.ChecksumException;
import sym.qrcode.DecodeHintType;
import sym.qrcode.NotFoundException;
import sym.qrcode.Reader;
import sym.qrcode.Result;
import sym.qrcode.ResultMetadataType;
import sym.qrcode.ResultPoint;
import sym.qrcode.common.BitMatrix;
import sym.qrcode.common.DecoderResult;
import sym.qrcode.common.DetectorResult;
import sym.qrcode.decoder.Decoder;
import sym.qrcode.dectector.Detector;
import sym.qrcode.ResultPointCallback;

import java.util.Hashtable;
import java.util.Vector;

public class QRCodeReader implements Reader {

	private static final ResultPoint[] NO_POINTS = new ResultPoint[0];
	private Hashtable hints = null;
	private Decoder decoder = new Decoder();

	protected Decoder getDecoder() {
		return decoder;
	}

	public void setHints(Hashtable hints) {
		this.hints = hints;
	}

	/**
	 * Locates and decodes a QR code in an image.
	 * 
	 * @return a String representing the content encoded by the QR code
	 * @throws NotFoundException
	 *             if a QR code cannot be found
	 * @throws FormatException
	 *             if a QR code cannot be decoded
	 * @throws ChecksumException
	 *             if error correction fails
	 */
	public Result decode(BinaryBitmap image) throws NotFoundException,
			ChecksumException {
		if (this.hints == null) {
			return decode(image, null);
		} else {
			return decode(image, hints);
		}
	}

	public Result decode(BinaryBitmap image, Hashtable hints)
			throws NotFoundException, ChecksumException {
		DecoderResult decoderResult;
		ResultPoint[] points;
		decoder = new Decoder();

		DetectorResult detectorResult = new Detector(image.getBlackMatrix())
				.detect(hints);

		decoderResult = decoder.decode(detectorResult.getBits(), hints);

		points = detectorResult.getPoints();

		if (hints != null
				&& hints.containsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) {
			ResultPointCallback cb = (ResultPointCallback) hints
					.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);
			for (ResultPoint point : points)
				cb.foundPossibleResultPoint(point);
		}
		Result result = new Result(decoderResult.getText(),
				decoderResult.getRawBytes(), points);
		if (decoderResult.getByteSegments() != null) {
			result.putMetadata(ResultMetadataType.BYTE_SEGMENTS,
					decoderResult.getByteSegments());
		}
		if (decoderResult.getECLevel() != null) {
			result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL,
					decoderResult.getECLevel().toString());
		}
		if (decoderResult.getDecrypt_text() != null
				&& decoderResult.getDecrypt_text().length() > 0) {
			result.putMetadata(ResultMetadataType.DECRYPTED_CONTENT,
					decoderResult.getDecrypt_text().toString());
		}
		return result;
	}

	public void reset() {
		// do nothing
	}

	/**
	 * This method detects a barcode in a "pure" image -- that is, pure
	 * monochrome image which contains only an unrotated, unskewed, image of a
	 * barcode, with some white border around it. This is a specialized method
	 * that works exceptionally fast in this special case.
	 */
	public static BitMatrix extractPureBits(BitMatrix image)
			throws NotFoundException {

		int height = image.getHeight();
		int width = image.getWidth();
		int minDimension = Math.min(height, width);

		// And then keep tracking across the top-left black module to determine
		// module size
		// int moduleEnd = borderWidth;
		int[] leftTopBlack = image.getTopLeftOnBit();
		if (leftTopBlack == null) {
			throw NotFoundException.getNotFoundInstance();
		}
		int x = leftTopBlack[0];
		int y = leftTopBlack[1];
		while (x < minDimension && y < minDimension && image.get(x, y)) {
			x++;
			y++;
		}
		if (x == minDimension || y == minDimension) {
			throw NotFoundException.getNotFoundInstance();
		}

		int moduleSize = x - leftTopBlack[0];
		if (moduleSize == 0) {
			throw NotFoundException.getNotFoundInstance();
		}

		// And now find where the rightmost black module on the first row ends
		int rowEndOfSymbol = width - 1;
		while (rowEndOfSymbol > x && !image.get(rowEndOfSymbol, y)) {
			rowEndOfSymbol--;
		}
		if (rowEndOfSymbol <= x) {
			throw NotFoundException.getNotFoundInstance();
		}
		rowEndOfSymbol++;

		// Make sure width of barcode is a multiple of module size
		if ((rowEndOfSymbol - x) % moduleSize != 0) {
			throw NotFoundException.getNotFoundInstance();
		}
		int dimension = 1 + ((rowEndOfSymbol - x) / moduleSize);

		// Push in the "border" by half the module width so that we start
		// sampling in the middle of the module. Just in case the image is a
		// little off, this will help recover. Need to back up at least 1.
		int backOffAmount = moduleSize == 1 ? 1 : moduleSize >> 1;
		x -= backOffAmount;
		y -= backOffAmount;

		if ((x + (dimension - 1) * moduleSize) >= width
				|| (y + (dimension - 1) * moduleSize) >= height) {
			throw NotFoundException.getNotFoundInstance();
		}

		// Now just read off the bits
		BitMatrix bits = new BitMatrix(dimension);
		for (int i = 0; i < dimension; i++) {
			int iOffset = y + i * moduleSize;
			for (int j = 0; j < dimension; j++) {
				if (image.get(x + j * moduleSize, iOffset)) {
					bits.set(j, i);
				}
			}
		}
		return bits;
	}

}
